#pragma once

#include <stx/core/stx.hpp>

inline namespace stx
{
template<IsDurationLike Dst_>
[[nodiscard]] constexpr auto to(IsDurationLike auto const src)
{
    using Src_ = decltype(src);

    if constexpr (IsStdDuration<Src_> && IsStdDuration<Dst_>)
    {
        return std::chrono::duration_cast<Dst_>(src);
    }
    else if constexpr (IsStdDuration<Src_> && IsStxDuration<Dst_>)
    {
        return Dst_(std::chrono::duration_cast<Milliseconds>(src));
    }
    else if constexpr (IsStxDuration<Src_> && IsStdDuration<Dst_>)
    {
        return std::chrono::duration_cast<Dst_>(src.ms);
    }
    else if constexpr (IsStxDuration<Src_> && IsStxDuration<Dst_>)
    {
        return Dst_(std::chrono::duration_cast<Milliseconds>(src.ms));
    }
    else
    {
        return InvalidType{};
    }
}

struct Duration
{
    using StxDurationTag_ = void;

    Milliseconds ms = 0ms;

    constexpr Duration() noexcept = default;

    // NOLINTNEXTLINE
    constexpr Duration(IsDurationLike auto const dur) noexcept
    {
        if constexpr (IsStdDuration<decltype(dur)>)
        {
            this->ms = to<Milliseconds>(dur);
        }
        else
        {
            static_assert(IsStxDuration<decltype(dur)>);
            this->ms = dur.ms;
        }
    }

    [[nodiscard]] constexpr std::size_t getHashCode() const noexcept
    {
        return Number(this->ms.count());
    }
};

[[nodiscard]] constexpr auto operator<=>(IsDurationLike auto const a,
                                         IsDurationLike auto const b) noexcept
{
    using A = NoCvr<decltype(a)>;
    using B = NoCvr<decltype(b)>;

    if constexpr (IsStdDuration<A> && IsStdDuration<B>)
    {
        if (a < b)
        {
            return std::strong_ordering::less;
        }
        else if (a > b)
        {
            return std::strong_ordering::greater;
        }

        return std::strong_ordering::equal;
    }
    else if constexpr (IsStxDuration<A> && IsStxDuration<B>)
    {
        return a.ms <=> b.ms;
    }
    else if constexpr (IsStxDuration<A>)
    {
        return a.ms <=> to<Milliseconds>(b);
    }
    else
    {
        return to<Milliseconds>(a) <=> b.ms;
    }
}

[[nodiscard]] constexpr bool operator==(IsDurationLike auto const a,
                                        IsDurationLike auto const b) noexcept
{
    return 0 == (a <=> b);
}

[[nodiscard]] inline auto elapsed(IsStdTimePoint auto const since)
{
    return NoCvr<decltype(since)>::clock::now() - since;
}

class Stopwatch final
{
    RelTimePoint start_tp_ = SteadyClock::now();

public:
    void reset() noexcept;

    [[nodiscard]] auto elapsed() const noexcept
    {
        return SteadyClock::now() - this->start_tp_;
    }
};

inline void sleepFor(IsDurationLike auto const dur) noexcept
{
    if constexpr (IsStdDuration<decltype(dur)>)
    {
        std::this_thread::sleep_for(dur);
    }
    else if constexpr (IsStxDuration<decltype(dur)>)
    {
        std::this_thread::sleep_for(dur.ms);
    }
    else
    {
        static_assert(AlwaysFalse<decltype(dur)>);
    }
}

inline void sleepUntil(IsStdTimePoint auto const tp) noexcept
{
    std::this_thread::sleep_until(tp);
}

void sleepForEver() noexcept;

#pragma pack(push)
#pragma pack(1)
class DateTime final
{
    std::size_t year_        : 12 = {};
    std::size_t month_       : 5  = {};
    std::size_t day_         : 5  = {};
    std::size_t hour_        : 5  = {};
    std::size_t minute_      : 6  = {};
    std::size_t second_      : 6  = {};
    std::size_t millisecond_ : 10 = {};
    std::size_t weekday_     : 3  = {}; // [0-6]

public:
    constexpr DateTime() noexcept = default;

    SPACESHIP_OPERATORS(DateTime);

    [[nodiscard]] std::size_t getHashCode() const noexcept;

    DateTime& setYear(std::size_t const year) noexcept;
    DateTime& setMonth(std::size_t const month) noexcept;
    DateTime& setDay(std::size_t const day) noexcept;
    DateTime& setHour(std::size_t const hour) noexcept;
    DateTime& setMinute(std::size_t const minute) noexcept;
    DateTime& setSecond(std::size_t const second) noexcept;
    DateTime& setMillisecond(std::size_t const millisecond) noexcept;
    DateTime& setWeekday(std::size_t const weekday) noexcept;

    [[nodiscard]] std::size_t getYear() const noexcept;
    [[nodiscard]] std::size_t getMonth() const noexcept;
    [[nodiscard]] std::size_t getDay() const noexcept;
    [[nodiscard]] std::size_t getHour() const noexcept;
    [[nodiscard]] std::size_t getMinute() const noexcept;
    [[nodiscard]] std::size_t getSecond() const noexcept;
    [[nodiscard]] std::size_t getMillisecond() const noexcept;
    [[nodiscard]] std::size_t getWeekday() const noexcept;
};
#pragma pack(pop)

static_assert(7 == sizeof(DateTime));

[[nodiscard]] SysTimePoint  FileToSys(FileTimePoint const tp_file);
[[nodiscard]] FileTimePoint SysToFile(SysTimePoint const tp_sys);

// This function may be greater than std::time(nullptr) because of leap seconds!
template<IsRawStdDuration Duration_>
requires std::same_as<Duration_, Milliseconds> ||
    std::same_as<Duration_, Seconds>
[[nodiscard]] inline std::uint64_t getTimestamp(SysTimePoint const tp_sys)
{
    auto const dur_since_sysclock_epoch =
        to<Duration_>(tp_sys.time_since_epoch());
    return Number(dur_since_sysclock_epoch.count());
}

template<IsRawStdDuration Duration_>
requires std::same_as<Duration_, Milliseconds> ||
    std::same_as<Duration_, Seconds>
[[nodiscard]] inline std::uint64_t getTimestamp()
{
    return getTimestamp<Duration_>(SystemClock::now());
}

[[nodiscard]] DateTime getDateTime(SysTimePoint const tp_sys);
[[nodiscard]] DateTime getDateTime();

enum class DateTimeFmt
{
    K_COMMON,            // "2021-12-12 12:12:12"
    K_COMMON_MS,         // "2021-12-12 12:12:12.789"
    K_COMMON_COMPACT,    // "20211031091224"
    K_COMMON_COMPACT_MS, // "20211031091224789"
    K_ISO,               // "2021-12-12T12:12:12Z"
    K_ISO_MS,            // "2021-12-12T12:12:12.789Z"
    K_TIMESTAMP,         // "1637602389"
    K_TIMESTAMP_MS,      // "1637602389089"
};

[[nodiscard]] std::string getDateTimeText(DateTimeFmt const  fmt,
                                          SysTimePoint const tp_sys);

[[nodiscard]] std::string getDateTimeText(DateTimeFmt const fmt);
} // namespace stx